package grpc

import (
	"context"
	"github.com/jasonlvhit/gocron"
	"github.com/nuttech/bell"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/mock"
	v1alpha12 "gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/grpc/mocks"
	"gitverse.ru/synapse/kubelatte/pkg/grpc/proto"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sync/atomic"
	"testing"
)

func TestSyncController_HardSync(t *testing.T) {
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name          string
		args          args
		trigger       v1alpha12.Trigger
		template      v1alpha12.Template
		scope         v1alpha12.Scope
		namespace     corev1.Namespace
		serverReturns error
	}{
		{
			trigger: v1alpha12.Trigger{
				TypeMeta:   metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{},
				Spec: v1alpha12.TriggerSpec{
					MutationConfigs: []v1alpha12.MutationConfig{{
						Name:           "",
						UpdateStrategy: "",
						TemplateRefs:   []string{"templNs/templName"},
						LabelSelector: metav1.LabelSelector{
							MatchLabels: nil,
							MatchExpressions: []metav1.LabelSelectorRequirement{
								{
									Key:      "labK",
									Operator: "Exists",
									Values:   nil,
								},
							},
						},
					}}},
				Status: v1alpha12.TriggerStatus{},
			},
			template:      v1alpha12.Template{},
			scope:         v1alpha12.Scope{},
			namespace:     corev1.Namespace{},
			serverReturns: nil,
		},
		{
			trigger: v1alpha12.Trigger{
				TypeMeta:   metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{},
				Spec: v1alpha12.TriggerSpec{
					MutationConfigs: []v1alpha12.MutationConfig{{
						Name:           "",
						UpdateStrategy: "",
						TemplateRefs:   []string{"templNs/templName"},
						LabelSelector: metav1.LabelSelector{
							MatchLabels: nil,
							MatchExpressions: []metav1.LabelSelectorRequirement{
								{
									Key:      "labK",
									Operator: "Exists",
									Values:   nil,
								},
							},
						},
					}}},
				Status: v1alpha12.TriggerStatus{},
			},
			template:      v1alpha12.Template{},
			scope:         v1alpha12.Scope{},
			namespace:     corev1.Namespace{},
			serverReturns: errors.New("some error"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logs.Logger = zap.NewNop()

			StartSyncServer(port)
			//defer shutDown()

			storage.Storage = &storage.StorageController{}
			storage.Storage.Start(false, false)
			storage.Storage.UpdateTrigger(bell.Message{Value: tt.trigger})
			storage.Storage.UpdateTemplate(bell.Message{Value: tt.template})
			storage.Storage.UpdateScope(bell.Message{Value: tt.scope})
			storage.Storage.UpdateNamespace(bell.Message{Value: tt.namespace})

			client := mocks.NewConfigSyncClient(t)
			client.On("Sync", mock.Anything, mock.Anything).
				Return(&proto.EmptyMessage{}, tt.serverReturns).Maybe()

			w := &SyncController{}
			w.Start()
			w.HardSync(tt.args.ctx, client)
		})
	}
}

func TestSyncController_SendMessage(t *testing.T) {
	tests := []struct {
		name          string
		obj           interface{}
		serverReturns error
	}{
		{
			name: "Trigger sending",
			obj: map[string]interface{}{
				"type": "something",
				"obj":  v1alpha12.Trigger{},
			},
		},
		{
			name: "Template sending",
			obj: map[string]interface{}{
				"type": "something",
				"obj":  v1alpha12.Template{},
			},
		},
		{
			name: "Scope sending",
			obj: map[string]interface{}{
				"type": "something",
				"obj":  v1alpha12.Scope{},
			},
		},
		{
			name: "Namespace sending",
			obj: map[string]interface{}{
				"type": "something",
				"obj":  corev1.Namespace{},
			},
		},
		{
			name: "Sth else sending",
			obj: map[string]interface{}{
				"type": "something",
				"obj":  "Sth else",
			},
		},
	}
	logs.Logger = zap.NewNop()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			StartSyncServer(port)

			client := mocks.NewConfigSyncClient(t)
			client.On("Sync", mock.Anything, mock.Anything).
				Return(&proto.EmptyMessage{}, tt.serverReturns).Maybe()

			w := &SyncController{}
			w.Start()
			w.SendMessage(tt.obj, client)
		})
		t.Run("hard_sync_true", func(t *testing.T) {
			logs.Logger = zap.NewNop()

			StartSyncServer(port)

			storage.Storage = &storage.StorageController{}
			storage.Storage.Start(false, false)

			client := mocks.NewConfigSyncClient(t)
			client.On("Sync", mock.Anything, mock.Anything).
				Return(&proto.EmptyMessage{}, tt.serverReturns).Maybe()

			w := &SyncController{}
			w.Start()
			w.IsNeedHardSync.Store(true)
			w.SendMessage(tt.obj, client)
		})
	}

	t.Run("grpc server error", func(t *testing.T) {
		logs.Logger = zap.NewNop()

		StartSyncServer(port)

		client := mocks.NewConfigSyncClient(t)
		client.On("Sync", mock.Anything, mock.Anything).
			Return(&proto.EmptyMessage{}, errors.New("some error")).Maybe()

		w := &SyncController{}
		w.Start()
		w.SendMessage(map[string]interface{}{
			"type": "something",
			"obj":  corev1.Namespace{},
		}, client)
	})
}

func TestSyncController_UpdateSyncPeriod(t *testing.T) {
	t.Run("ok", func(t *testing.T) {
		logs.Logger = zap.NewNop()

		w := &SyncController{}
		w.Start()
		w.UpdateSyncPeriod(bell.Message{Value: uint64(123)})

		if w.ReSyncPeriod != 123 {
			t.Errorf("wrong ReSyncPeriod, expected %v, got %v", 123, w.ReSyncPeriod)
		}
	})
}

func TestSyncController_AddNewObject(t *testing.T) {
	tests := []struct {
		name    string
		message bell.Message
	}{
		{
			name:    "ok",
			message: bell.Message{Value: map[string]interface{}{}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logs.Logger = zap.NewNop()

			w := &SyncController{}
			w.syncObjectCh = make(chan interface{}, 1)

			w.AddNewObject(tt.message)
			<-w.syncObjectCh
		})
	}
}

func TestSyncController_WaitForServerReadiness(t *testing.T) {
	logs.Logger = zap.NewNop()

	StartSyncServer(port)

	syncc := &SyncController{}
	syncc.WaitForServerReadiness()
}

func TestSyncController_LastSync(t *testing.T) {
	type fields struct {
		ReSyncPeriod   uint64
		lastSyncJob    *gocron.Job
		scheduler      *gocron.Scheduler
		LastTime       int64
		IsNeedHardSync bool
		syncObjectCh   chan interface{}
		client         proto.ConfigSyncClient
		conn           *grpc.ClientConn
	}
	tests := []struct {
		name   string
		fields fields
	}{
		{
			name: "need hard sync at first",
			fields: fields{
				LastTime:       100,
				IsNeedHardSync: true,
			},
		},
		{
			name: "need hard sync by time",
			fields: fields{
				LastTime:       100,
				IsNeedHardSync: false,
			},
		},
	}
	logs.Logger = zap.NewNop()
	StartSyncServer(port)

	storage.Storage = &storage.StorageController{}
	storage.Storage.Start(false, false)
	defer func() {
		storage.Storage = nil
	}()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := &SyncController{
				ReSyncPeriod:   tt.fields.ReSyncPeriod,
				lastSyncJob:    tt.fields.lastSyncJob,
				scheduler:      tt.fields.scheduler,
				IsNeedHardSync: atomic.Bool{},
				LastTime:       tt.fields.LastTime,
				syncObjectCh:   tt.fields.syncObjectCh,
				client:         tt.fields.client,
				conn:           tt.fields.conn,
			}
			w.IsNeedHardSync.Store(tt.fields.IsNeedHardSync)
			w.LastSync()
		})
	}
}
